Unity切片类全息效果 | 您所在的位置:网站首页 › unity 切片 › Unity切片类全息效果 |
前言 全息图的效果,想必诸位在科幻电影和游戏中都没少见过。当年E3上,育碧的《全境封锁》就以其独特的全息地图惊艳全场。 而大多时候,我们使用的全息图是形如【1】一般的,单个物体的图像在世界空间坐标系中沿Y轴形成的波纹和抖动效果: 不过这种样式太过单一,无法体现全息图类似“切片叠加”的效果。在查看了【2】文章后,提起了我制作类似效果的兴趣,因此绕了不少弯路,用了一天左右的业余时间做到了一个效果接近的技术DEMO。 效果特点在谈到一个图像效果的时候,我们必须用描述其特点,这些特点会引导我们用什么技术去实现它。 一个模型会被世界空间坐标系下,基于y轴等间距的切片。每一个切片,都是该模型在该平面的形状。切片是半透明的,某个地方越厚,形成的切片颜色越深重。根据这些特点,我先后构思了以下几种方案,但在思考和实践中,一一予以否决。 反复绘制法将模型直接“坍缩”到特定y值的平面上,通过CommandBuffer发起反复的绘制,最后形成一个切片的集合。 反复绘制法的优势在于简单直白,而缺点则很多:首先就是大大增加了DrawCall,其实光这一点,就足以判该方法死刑;其次是不支持复杂形状的模型;最后就是为了设置切片绘制范围,控制成本太高昂。 后处理法如同计算厚度一般,分别用两个Shader获取其前表面深度和后表面深度,然后用光线步进的方法,去计算其涉及了多少个切片平面。 后处理法的优势在于集成方便,缺点在于性能消耗常驻,而且不便控制遮挡关系。 ShadowMap法也是本文的做法,通过与ShadowMap一样的原理,使用俯视的正交相机拍摄下前后表面深度,然后批量绘制巨大的Plane,将每个点返回到正交相机中对比深度。 ShadowMap法的好处在于性能消耗相对较低,控制方便;缺点在于需要使用常驻的RenderTexture,而且对切片展示的区域有着严格的限制。 思路首先,在俯视的正交相机上使用CommandBuffer,将需要渲染成切片全息图形式的Renderer的前后表面深度拓印到两张RFloat格式的RenderTexture下。 其次,准备好覆盖整个正交相机拍摄区域的平面,在渲染这些平面时,将片元的世界坐标空间位置转换到正交相机中的深度。 最后,比较深度和前后表面深度之间的关系,如果不在其中,则舍去,否则呈现半透明渲染。 渲染深度使用两个深度Shader渲染物体的厚度,这两个Shader的唯一区别仅在与Cull指令: Shader "Hidden/BackDepth" { SubShader { Tags { "RenderType" = "Opaque" } Pass { Cull Front // 如果是Front则Cull Back CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_instancing #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct v2f { float4 vertex : SV_POSITION; float linearDepth : TEXCOORD0; float3 worldPos0 : TEXCOORD1; float3 worldPos : TEXCOORD2; UNITY_VERTEX_OUTPUT_STEREO }; sampler2D _MainTex; float4 _MainTex_ST; v2f vert(appdata v) { v2f o; UNITY_SETUP_INSTANCE_ID(v); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o); o.vertex = UnityObjectToClipPos(v.vertex); o.linearDepth = COMPUTE_DEPTH_01; o.worldPos0 = mul(unity_ObjectToWorld, float4(0, 0, 0, 1)); o.worldPos = mul(unity_ObjectToWorld, v.vertex); return o; } float4 frag(v2f i) : SV_Target { return float4(i.linearDepth, 0, 0, 0); } ENDCG } } }然后使用CommandBuffer,为垂直正交相机增加命令缓冲,该命令缓冲会在正交相机渲染的时候,额外使用上述Shader渲染前后表面的深度图: var backShader = Shader.Find("Hidden/BackDepth"); var frontShader = Shader.Find("Hidden/FrontDepth"); var backMat = new Material(backShader); var frontMat = new Material(frontShader); cb = new CommandBuffer(); cb.name = "Overlook"; // 申请RT int _backRT = Shader.PropertyToID("_BackDepthTex"); int _frontRT = Shader.PropertyToID("_FrontDepthTex"); int _copyRT = Shader.PropertyToID("_CopyTex"); cb.GetTemporaryRT(_backRT, 1024, 1024, 0, FilterMode.Point, RenderTextureFormat.RFloat); cb.GetTemporaryRT(_frontRT, 1024, 1024, 0, FilterMode.Point, RenderTextureFormat.RFloat); cb.GetTemporaryRT(_copyRT, 1024, 1024, 24, FilterMode.Point, RenderTextureFormat.ARGB32); // 出前后深度 cb.SetRenderTarget(_backRT); cb.ClearRenderTarget(true, true, Color.clear); foreach (var renderer in renderers) cb.DrawRenderer(renderer, backMat); cb.SetRenderTarget(_frontRT); cb.ClearRenderTarget(true, true, Color.clear); foreach (var renderer in renderers) cb.DrawRenderer(renderer, frontMat); cb.SetGlobalTexture("_BackDepthTex", _backRT); cb.SetGlobalTexture("_FrontDepthTex", _frontRT); if (overlookCamera) { // 传一个小RT保证屏幕比例,当然如果深度RT也做了设置就不用改了 if (overlookCamera.targetTexture == null) overlookCamera.targetTexture = new RenderTexture(64, 64, 0); // 保证不发生渲染 overlookCamera.cullingMask = 0; overlookCamera.AddCommandBuffer(CameraEvent.AfterEverything, cb); }当然,有必要指出一点,那就是这样的做法完全是处于技术预研阶段使用的,实际上这个方法会导致正交相机始终处于激活状态;另外,分别渲染会导致深度图阶段无法合批。 比对并渲染使用注意覆盖相机拍摄范围的平面Mesh,组合成切片平面集合。为其编写切片平面的Shader: Shader "Unlit/LayerShader" { SubShader { Tags {"Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent"} Cull Off ZWrite On Blend SrcAlpha OneMinusSrcAlpha Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_instancing #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct v2f { float4 vertex : SV_POSITION; float4 worldPos : TEXCOORD1; UNITY_VERTEX_OUTPUT_STEREO }; fixed4 _Color; sampler2D _BackDepthTex, _FrontDepthTex; float4x4 _Overlook_Matrix_V, _Overlook_Matrix_P; float4 _OverlookProjectionParams; float _LayerWeight; v2f vert (appdata v) { v2f o; UNITY_SETUP_INSTANCE_ID(v); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o); o.vertex = UnityObjectToClipPos(v.vertex); o.worldPos = mul(unity_ObjectToWorld, v.vertex); return o; } fixed4 frag (v2f i) : SV_Target { float4 viewPos = mul(_Overlook_Matrix_V, i.worldPos); float depth01 = -(viewPos.z * _OverlookProjectionParams.w); float4 clipPos = mul(_Overlook_Matrix_P, viewPos); float4 screenPos = ComputeScreenPos(clipPos); float dBack = tex2Dproj(_BackDepthTex, screenPos); float dFront = tex2Dproj(_FrontDepthTex, screenPos); clip(depth01 - dFront); clip(dBack - depth01); return float4(_Color.rgb, _LayerWeight); } ENDCG } } }其中使用到了诸如_Overlook_Matrix_P之类的矩阵和_OverlookProjectionParams的参数,这些由C#阶段计算完成并传入,并且不能在CommandBuffer中进行该步骤,而是在OnPreRender阶段计算,否则参数可能不会实时更新! public void OnPreRender() { Shader.SetGlobalFloat("_LayerWeight", weightPerLayer); Shader.SetGlobalColor("_Color", Color.white); Shader.SetGlobalVector("_OverlookProjectionParams", new Vector4( 1, overlookCamera.nearClipPlane, overlookCamera.farClipPlane, 1f / overlookCamera.farClipPlane) ); Shader.SetGlobalMatrix("_Overlook_Matrix_V", overlookCamera.worldToCameraMatrix); Shader.SetGlobalMatrix("_Overlook_Matrix_P", GL.GetGPUProjectionMatrix(overlookCamera.projectionMatrix, true)); } 效果展示构成该效果的几何体场景是这样的: 性能表现Frame Debugger的渲染队列如图,由于分别渲染的原因,每个Renderer被渲染了2次,而最后的切片平面集合由于GPU Instancing,其实只产生了一个DrawCall。 局限性该方法也存在着局限性,首先切片平面集合的位置和尺寸,和正交相机,都引入了一定的控制成本。 其次,在面临大量物体需要渲染的时候,在CommandBuffer里分别渲染会产生巨量的DrawCall,解决的方法是使用空间管理算法,或者彻底不使用CommandBuffer,而是使用两个相机和两批Layer不同的Renderer来渲染深度图。 第三,该方法下必须保证前后表面都处于相机远近平面内才能保证正确计算厚度,产生切片效果。 最后,也是最麻烦的一点,由于是一个自顶而下的正交相机,会导致Y轴上不同物体遮挡的时候会导致位于下方的物体厚度计算失效。解决方法是避免Y轴上出现遮挡,换而言之,就是拒绝在Y轴上形成不连续的深度值域。 由于被遮挡,下方的切片集合被“抠”掉了后记本文践行了一种视觉效果为切片集合的全息图效果,受限于其局限性,该效果可能更适用于科幻游戏中的建筑展示。通过对正交相机所生成的深度图进行模糊或者其他处理,有望获取更多其他的效果。 通过调整面片的方向,还能实现不同的切片效果。Github地址参考资料【1】https://zhuanlan.zhihu.com/p/141940278 【2】https://zhuanlan.zhihu.com/p/67458879 |
CopyRight 2018-2019 实验室设备网 版权所有 |